import logging

from neo4j import Driver
import neo4j
from neo4j.exceptions import DriverError, Neo4jError
from numpy import ndarray
import pandas as pd

from foundation.infra.db.util.pagination import Pagination
from graph.util.node_util import normalize_node_label
from graph.util.util import ensure_connection, get_or_default_ndarray

logger = logging.getLogger(__name__)


class Neo4jFoundationalNodeAdapter:
    """
    Adapter to perform operations on eugene(genieve) graph in neo4j
    """

    def __init__(self, driver: Driver, database: str = "neo4j"):
        self.driver = driver
        self.database = database

    def find_node_id_by_node_name(
        self, value: str, fuzzy_match: bool = False
    ) -> pd.DataFrame | None:
        if value is None:
            return None
        ensure_connection(self.driver)
        return self._find_node_id_by_node_name(value=value, fuzzy_match=fuzzy_match)

    def verify_exists_by_node_index(self, label: str, node_index: str) -> bool:
        if label is None or node_index is None:
            return False

        ensure_connection(self.driver)
        return self._verify_exists_by_node_index(label, node_index)

    def find_by_label(
        self, label: str, page: int, page_size: int
    ) -> pd.DataFrame | None:
        ensure_connection(self.driver)
        return self._find_by_label(label, page, page_size)

    def find_all_by_label(self, label: str) -> pd.DataFrame | None:
        ensure_connection(self.driver)
        return self._find_all_by_label(label)

    def upsert_embeddings(
        self, label: str, node_index: str, embeddings: ndarray
    ) -> str | None:
        # Python driver: https://neo4j.com/docs/api/python-driver/current/index.html
        # Cypher syntax: https://neo4j.com/docs/cypher-manual/current/
        # Cypher cheatsheet https://neo4j.com/docs/cypher-cheat-sheet/
        if label is None or node_index is None or embeddings is None:
            return

        upsert_node_embeddings = (
            """
            MERGE (n:`%s` {
                node_index: $node_index
            })
            ON MATCH
                SET n.embeddings = $embeddings
            RETURN n.node_index
            """
            % f"{normalize_node_label(label)}"
        )
        logger.debug(f"upsert: {upsert_node_embeddings}")
        try:
            record = self.driver.execute_query(
                upsert_node_embeddings,
                {
                    "node_index": node_index,
                    "embeddings": get_or_default_ndarray(embeddings),
                },
                database_=self.database,
                result_transformer_=neo4j.Result.single,
            )

            if record is None:
                return None

            logger.debug(f"upsert response: {record}")
            return record["n.node_index"]
        except (DriverError, Neo4jError) as exception:
            logging.error("%s raised an error: \n%s", upsert_node_embeddings, exception)
            raise

    def _find_all_by_label(self, label: str) -> pd.DataFrame | None:
        query = self._build_find_all_by_label(label)
        logger.debug(f"find all: {query}")
        try:
            records = self.driver.execute_query(
                query_=query,
                result_transformer_=neo4j.Result.to_df,
            )
            logger.debug(f"cypher response: {records}")
            return records
        except (DriverError, Neo4jError) as exception:
            logging.error("%s raised an error: \n%s", query, exception)
            raise exception

    def _find_by_label(
        self, label: str, page: int, page_size: int
    ) -> pd.DataFrame | None:
        query = self._build_find_by_label(label, page, page_size)
        logger.debug(f"find all: {query}")
        try:
            records = self.driver.execute_query(
                query_=query,
                result_transformer_=neo4j.Result.to_df,
            )
            logger.debug(f"cypher response: {records}")
            return records
        except (DriverError, Neo4jError) as exception:
            logging.error("%s raised an error: \n%s", query, exception)
            raise exception

    def _build_find_all_by_label(self, label: str) -> str:
        lookup_node_query = """MATCH (n:`%s`)
                RETURN n.node_id as node_id, n.node_name as node_name
                ORDER BY n.node_name
            """ % (
            normalize_node_label(label),
        )
        return lookup_node_query

    def _build_find_by_label(self, label: str, page: int, page_size: int) -> str:
        limit, offset = Pagination.calculate_limit_and_offset(page, page_size)
        lookup_node_query = """MATCH (n:`%s`)
                RETURN n.node_id as node_id, n.node_name as node_name
                ORDER BY node_name
                SKIP %s
                LIMIT %s
            """ % (
            normalize_node_label(label),
            offset,
            limit,
        )
        return lookup_node_query

    def _verify_exists_by_node_index(self, label: str, node_index: str) -> bool:
        query = self.build_verify_exists_by_node_index(label=label)
        logger.debug(f"exists query: {query}")
        try:
            record = self.driver.execute_query(
                query_=query,
                parameters_={"node_index": node_index},
                result_transformer_=neo4j.Result.single,
            )
            logger.debug(f"cypher response: {record}")
            if record is not None and record["n.node_index"] is not None:
                return True
            else:
                return False
        except (DriverError, Neo4jError) as exception:
            logging.error("%s raised an error: \n%s", query, exception)
            raise exception

    def build_verify_exists_by_node_index(self, label: str) -> str:
        lookup_node_query = (
            """MATCH (n: %s { node_index: $node_index })
            RETURN n.node_index
            """
            % (normalize_node_label(label)).strip()
        )
        return lookup_node_query

    def _find_node_id_by_node_name(
        self, value: str, fuzzy_match: bool = False
    ) -> pd.DataFrame | None:
        query = (
            self.build_find_node_id_by_node_name()
            if not fuzzy_match
            else self.build_fuzzy_find_node_id_by_node_name(value)
        )
        params = {"node_name": value} if not fuzzy_match else {}
        logger.info(f"find node id query: {query}")
        try:
            records = self.driver.execute_query(
                query_=query,
                parameters_=params,
                result_transformer_=neo4j.Result.to_df,
            )
            if records is None:
                return None
            logger.debug(f"cypher response: {records.shape}")
            return records
        except (DriverError, Neo4jError) as exception:
            logging.error("%s raised an error: \n%s", query, exception)
            raise exception

    def build_find_node_id_by_node_name(self) -> str:
        lookup_node_query = """MATCH (n { node_name: $node_name })
        WHERE n.is_hidden IS NULL OR NOT n.is_hidden
        RETURN n.node_id, n.node_name
        """
        return lookup_node_query

    def build_fuzzy_find_node_id_by_node_name(self, value: str) -> str:
        lookup_node_query = """MATCH (n)
        WHERE n.node_name =~ '(?i).*%s.*' and (n.is_hidden IS NULL OR NOT n.is_hidden)
        RETURN n.node_id, n.node_name
        """ % (
            value
        )
        return lookup_node_query
